﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NDBGen.Descriptors;
using NDBGen.Descriptors.Columns.DataTypes;
using NDBGen.Descriptors.Columns.Tokens;

namespace NDBGen
{
    public sealed class Column
    {
        public Column(Table table, String input)
        {
            this.Table = table;
            this.Input = input;

            this.DescriptorList = new List<IColumnTokenDescriptor>();

            this.ColumnTokenDescriptorLibrary = new ExportLibrary<IColumnTokenDescriptor, ITokenDescriptorBehaviors>(PluginLibraries.Catalog);
            this.DataTypeDescriptorLibrary = new ExportLibrary<IColumnDataTypeDescriptor, ITokenDescriptorMetadata>(PluginLibraries.Catalog);
        }

        internal Column(Table table, String name, DataType dataType)
            : this(table, name, dataType, null, null) { }

        internal Column(Table table, String name, DataType dataType, Int32? length)
            : this(table, name, dataType, length, null) { }

        internal Column(Table table, String name, DataType dataType, Int32? length, Int32? scale)
        {
            this.Table = table;

            this.Name = name;
            this.DataType = dataType;
            this.Length = length;
            this.Scale = scale;

            this.ColumnTokenDescriptorLibrary = new ExportLibrary<IColumnTokenDescriptor, ITokenDescriptorBehaviors>(PluginLibraries.Catalog);
            this.DataTypeDescriptorLibrary = new ExportLibrary<IColumnDataTypeDescriptor, ITokenDescriptorMetadata>(PluginLibraries.Catalog);

            this.DataTypeDescriptor = this.DataTypeDescriptorLibrary.FindOne(x => (x.DataTypes & dataType) == dataType);
            this.DataTypeDescriptor.Init(table, this);

            this.DescriptorList = new List<IColumnTokenDescriptor>();
        }

        internal Column AddDescriptor<T>(String tokenValue)
            where T : IColumnTokenDescriptor, new()
        {
            var descriptor = new T();
            descriptor.Init(this.Table, this, tokenValue);
            this.DescriptorList.Add(descriptor);

            return this;
        }

        internal Column AddDescriptors(params IColumnTokenDescriptor[] descriptors)
        {
            foreach (var descriptor in descriptors) descriptor.Init(this.Table, this);
            this.DescriptorList.AddRange(descriptors);
            
            return this;
        }

        private ExportLibrary<IColumnTokenDescriptor, ITokenDescriptorBehaviors> ColumnTokenDescriptorLibrary { get; set; }
        private ExportLibrary<IColumnDataTypeDescriptor, ITokenDescriptorMetadata> DataTypeDescriptorLibrary { get; set; }

        public Table Table { get; private set; }

        public String Input { get; private set; }

        private void InitColumnTokens()
        {
            foreach (var token in this.DescriptorList)
            {
                token.Init(this.Table, this);
            }
        }

        public IColumnDataTypeDescriptor DataTypeDescriptor { get; private set; }
        internal List<IColumnTokenDescriptor> DescriptorList { get; set; }
        public IColumnTokenDescriptor[] Descriptors { get { return this.DescriptorList.ToArray(); } }

        public Boolean HasExtendedDefinition { get { return this.DescriptorList.Any(t => !String.IsNullOrEmpty(t.ToString())); } }

        public String Name { get; private set; }
        public DataType DataType { get; private set; }
        public Int32? Length { get; private set; }
        public Int32? Scale { get; set; }
        public Boolean HasMaxLength { get { return this.Length.HasValue && this.DataType.IsStringType() && this.Length.Value != Int32.MaxValue; } }
        public Boolean IsBigType 
        { 
            get 
            {
                return (this.DataType.IsBigType() || (this.DataType.IsStringType() && this.Length.HasValue && this.Length.Value == Int32.MaxValue));
            }
        }
        public Boolean HasPrimaryKey
        {
            get { return this.DescriptorList.OfType<PrimaryKeyTokenDescriptor>().Any(); }
        }

        private Int32 GetIndexOfNextToken(Int32 start, DataType dataType, Char[] tokens)
        {
            for (var index = start; index < tokens.Length; index++)
            {
                // get the first IColumnTokenDescriptor implementation that matches the descriptor
                var descriptor = this.ColumnTokenDescriptorLibrary.FindOneWithMetadata(d => d.Value.Token == tokens[index] && (d.Metadata.DataTypes & dataType) == dataType);

                // if there is no descriptorExport, skip this descriptor
                if (descriptor == null) continue;
                else return index;
            }

            return -1;
        }

        public Boolean IsParsed { get; private set; }

        public Column Parse()
        {
            var parts = this.Input.Trim().Split('|');
            if (parts.Length < 2) throw new ArgumentException("Must contain, at a minimum, a column name and a datatype delimited by |.");
            
            // Name is the first column
            this.Name = parts[0];

            // Data type is the second column, it may have a length
            var type = parts[1].Split(',');
            try { this.DataType = DataTypeMapper.Parse(parts[1]); }
            catch (Exception inner) { throw new NotSupportedException("The data type '" + type[0] + "' is not supported.", inner); }
            if (type.Length > 1)
            {
                Int32 length;
                if (type[1].ToLower() != "max")
                {
                    if (Int32.TryParse(type[1], out length))
                    {
                        this.Length = length;

                        // check for scale
                        if (type.Length > 2)
                        {
                            Int32 scale;
                            if (Int32.TryParse(type[2], out scale))
                            {
                                this.Scale = scale;
                            }
                            else throw new ArgumentException("The value after the second comma in a data type (scale) must be an integer.");
                        }
                    }
                    else throw new ArgumentException("The value after the comma in a data type (length) must be an integer or 'max'.");
                }
            }

            var dataTypeHandler = this.DataTypeDescriptorLibrary.FindOneForDataType(this.DataType);
            if (dataTypeHandler == null) throw new NotSupportedException("The data type '" + this.DataType + "' is not supported.");

            this.DataTypeDescriptor = dataTypeHandler;
            this.DataTypeDescriptor.Init(this.Table, this);

            // The third column indicates any special properties
            if (parts.Length > 2)
            {
                var props = parts[2];
                var tokens = props.ToCharArray();

                for (var index = 0; index < tokens.Length; index++)
                {
                    var token = tokens[index];

                    // get the first IColumnTokenDescriptor implementation that matches the descriptor
                    var descriptorExport = this.ColumnTokenDescriptorLibrary.FindOneWithMetadata(t => t.Value.Token == token && (t.Metadata.DataTypes & this.DataType) == this.DataType);

                    // if there is no descriptorExport, skip this descriptor
                    if (descriptorExport == null) continue;

                    var tokenHasValue = descriptorExport.Metadata.HasValue;

                    String tokenValue = descriptorExport.Metadata.GeneratesWhenNotPresent ? token.ToString() : null;
                    if(tokenHasValue)
                    {
                        var nextTokenIndex = this.GetIndexOfNextToken(index + 1, this.DataType, tokens);
                        if(nextTokenIndex > 0) tokenValue = props.Substring(index + 1, nextTokenIndex - index - 1);
                        else tokenValue = props.Substring(index + 1);
                    }

                    descriptorExport.Value.Init(this.Table, this, tokenValue);

                    this.DescriptorList.Add(descriptorExport.Value);
                }
            }

            this.AddGeneratesAndTriggered();
            this.InitColumnTokens();

            this.IsParsed = true;

            return this;
        }

        private Boolean TokenIsDescribed(IColumnTokenDescriptor descriptor)
        {
            return this.DescriptorList.Any(t => t.Token == descriptor.Token);
        }

        private Boolean TokenIsDescribed(Type descriptorType)
        {
            return this.DescriptorList.Any(t => t.GetType() == descriptorType);
        }

        private Boolean TokenIsTriggered(Lazy<IColumnTokenDescriptor, ITokenDescriptorBehaviors> export)
        {
            return
                export.Metadata.IsTriggered &&
                (export.Metadata.DataTypes & this.DataType) == this.DataType &&
                export.Metadata.TriggeredByTypes != null &&
                export.Metadata.TriggeredByTypes.All(t => TokenIsDescribed(t));
        }

        private void AddGeneratesAndTriggered()
        {
            var generatesAndTriggered =
                from x in this.ColumnTokenDescriptorLibrary.Exports
                where (x.Metadata.GeneratesWhenNotPresent || this.TokenIsTriggered(x))
                    && !this.TokenIsDescribed(x.Value)
                select x.Value;

            this.DescriptorList.AddRange(generatesAndTriggered);
        }

        public override String ToString()
        {
            var sb = new StringBuilder();

            sb.Append(this.DataTypeDescriptor);

            if (this.HasExtendedDefinition)
            {
                sb.Append('|');

                foreach (var extended in this.DescriptorList)
                {
                    if(extended.ToString() != null) sb.Append(extended);
                }          
            }
            return sb.ToString();
        }

        public override Boolean Equals(Object obj)
        {
            if (!this.IsParsed)
            {
                this.AddGeneratesAndTriggered();
                this.InitColumnTokens();
                this.IsParsed = true;
            }

            var that = obj as Column;
            if (that == null) return false;

            return
                this.Name == that.Name &&
                this.DataType == that.DataType &&
                this.Length == that.Length &&
                this.DataType == that.DataType &&
                this.DescriptorList.SortOrdered().SequenceEqual(that.DescriptorList.SortOrdered());
        }
    }
}